package com.emotion.recognition.server.som;

import java.util.List;
import java.util.Map;
import java.util.Random;

import com.google.gwt.thirdparty.guava.common.base.Preconditions;
//import com.ibm.icu.text.DecimalFormat;

public class SOM {
	private final double initialLearningRate;
	private double learningRate;
	private final int inputSize;
	private final int maxEpochs;
	private FeatureMap featureMap;

	public SOM(int inputSize, int mapSize, double learningRate, int maxEpochs,
			double radius) {
		Preconditions.checkArgument(0 < learningRate && learningRate <= 1,
				"Learning rate must be in the range (0, 1]");
		Preconditions.checkArgument(0 < maxEpochs,
				"Max training epochs must be at least one");
		Preconditions.checkArgument(2 < mapSize,
				"Map size must be at least three");
		Preconditions.checkArgument(0 < inputSize,
				"Input size must be at least one");

		featureMap = new FeatureMap(mapSize, inputSize, radius);

		this.inputSize = inputSize;
		this.initialLearningRate = learningRate;
		this.learningRate = learningRate;
		this.maxEpochs = maxEpochs;
	}

	public FeatureMap classify(List<List<Double>> inputs,
			List<String> categories) {
		Preconditions.checkArgument(0 < inputs.size(),
				"Input size must be at least one");

		Preconditions.checkArgument(inputs.size() == categories.size(),
				"Number of inputs-categories pair does not match.");

		for (List<Double> input : inputs) {
			Preconditions.checkArgument(input.size() == this.inputSize,
					"Input dimensions do not match");
		}

		Random generator = new Random();

		for (int i = 0; i < this.maxEpochs; i++) {
			// Recalculate the radius.
			featureMap.setRadius(SomMath.calculateRadius(
					featureMap.getMapSize() / 2, i, maxEpochs));

			debugPrint();
			
			// Randomly select an input.
			int index = generator.nextInt(inputs.size());

			// Run the algorithm on input.
			classifyOneInput(inputs.get(index), categories.get(index));

			// Decrease learning rate
			learningRate = SomMath.calculateLearningRate(initialLearningRate,
					i, maxEpochs);		
		}

		return featureMap;
	}

	private void classifyOneInput(List<Double> input, String category) {
		FeatureNode minNode = null;
		double minDistance = Double.MAX_VALUE;
		double currentDistance, influence, currentDistanceSquared = 0;

		for (int i = 0; i < featureMap.getMapSize(); i++) {
			for (int j = 0; j < featureMap.getMapSize(); j++) {
				// Calculate distance from input to each node.
				currentDistance = SomMath.calculateDistance(input,
						featureMap.nodes[i][j]);

				// Minimum distance becomes BMU.
				if (currentDistance < minDistance) {
					minDistance = currentDistance;
					minNode = featureMap.nodes[i][j];
				}
			}
		}

		Preconditions.checkArgument(minNode != null,
				"Error: No minimum node was found");

		// Used for debugging purposes. See what node contains which values.
		minNode.setValue(category);

		// Now we have BMU, adjust weights of neighbourhood.
		for (int i = 0; i < featureMap.getMapSize(); i++) {
			for (int j = 0; j < featureMap.getMapSize(); j++) {
				currentDistanceSquared = SomMath.calculateDistanceSquared(
						minNode, featureMap.nodes[i][j]);

				// If node is within the neighbourhood.
				if (currentDistanceSquared < Math.sqrt(featureMap.getRadius())) {

					// Adjust weights
					influence = SomMath.calculateInfluence(
							currentDistanceSquared, featureMap.getRadius());
					for (int k = 0; k < input.size(); k++) {
						SomMath.calculateNewWeight(input.get(k),
								featureMap.nodes[i][j].getWeights()[k],
								learningRate, influence);
					}
				}
			}
		}
	}

	//
	// Builders below
	//

	public static Builder builder() {
		return new Builder();
	}

	public static class Builder {
		private int inputSize;
		private double learningRate = 0.25d;
		private int mapSize;
		private int maxEpochs = 1000;
		private double radius = 0;

		public Builder setInputSize(int inputSize) {
			this.inputSize = inputSize;
			return this;
		}

		public Builder setLatticeSize(int mapSize) {
			if (this.radius == 0) {
				this.radius = mapSize / 2;
			}
			this.mapSize = mapSize;
			return this;
		}

		/**
		 * Optional.
		 */
		public Builder setRadius(double radius) {
			this.radius = radius;
			return this;
		}

		/**
		 * Optional.
		 */
		public Builder setLearningRate(double learningRate) {
			this.learningRate = learningRate;
			return this;
		}

		/**
		 * Optional.
		 */
		public Builder setMaxEpochs(int maxEpochs) {
			this.maxEpochs = maxEpochs;
			return this;
		}

		public SOM buildClassifier() {
			return build();
		}

		private SOM build() {
			return new SOM(inputSize, mapSize, learningRate, maxEpochs, radius);
		}
	}

	public void debugPrint() {
		StringBuilder s = new StringBuilder();

		s.append("/------------------------------------------- SOM ------------------------------------------\\\n");

		s.append("|---------------------------------- Neural network view -----------------------------------|\n");

		for (int i = 0; i < featureMap.getMapSize(); i++) {
			for (int j = 0; j < featureMap.getMapSize(); j++) {
				s.append("|");
				String data = "";
				//DecimalFormat df = new DecimalFormat("#.##");
				//for (Double weight: featureMap.nodes[i][j].getWeights()) {
					//data += df.format(weight) + " ";					
				//}				
				Map<String, Integer> values = featureMap.nodes[i][j]
						.getValues();
				for (String category : values.keySet()) {
					data += category + values.get(category);
				}
				s.append(data);
				for (int k = 0; k < 10 - data.length(); ++k) {
					s.append(" ");
				}
			}
			s.append("| \n \n");
		}

		s.append("/------------------------------------------- SOM ------------------------------------------\\\n");

		System.out.println(s);
	}

}
